import os
import subprocess
from pathvalidate import is_valid_filename, sanitize_filename
from joblib import Parallel, delayed

from typing import Any, List, Optional

from common.io_utils.dirs import working_directory
from common.logger import logger


def check_download_url_via_wget(
    url: str,
    username: Optional[str] = None,
    password: Optional[str] = None,
    additional_credential: Optional[Any] = None,
    verbose_level: int = 1,
    n_tries: int = 5,
):
    """Check whether file exist at provided url using wget

    Parameters
    ----------
    url: str
        url to download
    username: str, optional
    password: str, optional
    additional_credential: any, optional
        NotImplemented
    verbose_level: int, optional
        - 0 == 'no-verbose' in wget, which "turn off verbose without being completely quiet,
            which means that error messages and basic information still get printed."
        - 1 == "default" in wget, which turn on verbose output, with all the available data.
        - 2 == "debug" in wget, which "turn on debug output, meaning various information important to the developers of
            does not work properly. Your system administrator may have chosen to compile Wget without debug support,
            Wget if it in which this option will not work. Please note that compiling with debug support is always
            safe—Wget compiled with the debug support will not print any debug info unless requested with this option."
    n_tries: int, optional
        number of times that wget try downloading before giving up, default is 5.
        The function do not support 'inf' for retrying infinitely.

    Returns
    -------
    None

    Raises
    ------
    ValueError
        - Raised when verbose level is not valid
        - Raised when provided URL is not valid

    Notes
    -----
    This function uses directly forms the command called in a `subprocess` by using the given inputs.
    Thus please do not use it directly with external user inputs.
    1. Not sure if exception actually occur if wget failed after n-try.
    2. Consider returning default file name. However, we are not able to implement a way to find out filename without
        downloading the file with wget,
        online forum said (--content-disposition), but did not work as initially expected.
    3. This function was intended to distinguish webpage from url for downloading file.
        However, it raised value error on GES DISC downloading page.
        As a result, this function can now only distinguish valid url from a bad url.
        It will no longer throw an error on a webpage like google home page.

    Reference
    --------
    wget user manual: https://www.gnu.org/software/wget/manual/wget.html

    Examples
    --------
    url = 'www.example.com'
    check_download_url_via_wget(url)
    """

    # Check if provided link is valid
    # [Not yet implemented] grab file name
    commands = [
        "wget",
        "-S",
        "--spider",
        f"--tries={n_tries}",
    ]
    if username is not None:
        commands.append(f"--user={username}")
    if password is not None:
        commands.append(f"--password={password}")
    if additional_credential is not None:
        return NotImplemented
    # if verbose not default,
    if verbose_level != 1:
        if verbose_level == 0:
            # "no-verbose"
            commands.append("-nv")
        elif verbose_level == 2:
            # "debug"
            commands.append("--debug")
        else:
            raise ValueError(
                f"Verbose_level ({verbose_level}) not understood. Valid value ranges from 0 to 2"
            )
    commands.append(url)
    # Replace Popen
    ret = subprocess.run(commands, capture_output=True)
    if ret.returncode != 0:
        raise ValueError(f"Invalid URL `{url}`")


def download_via_wget(
    url: str,
    dest_dir: str,
    dest_file_name: Optional[str] = None,
    username: Optional[str] = None,
    password: Optional[str] = None,
    additional_credential: Optional[Any] = None,
    continue_if_broke: bool = True,
    verbose_level: int = 1,
    n_tries: int = 5,
    download_speed: Optional[float] = None,
):
    """Download a file with wget

    Parameters
    ----------
    url: str
        url to download
    dest_dir: str
        Path to the storage directory.
    dest_file_name: str, optional
        User defined file name, should include file extension.
    username: str, optional
    password: str, optional
    additional_credential: any, optional
        NotImplemented
    continue_if_broke: bool, optional
        Decide if download process were interrupted, whether to download by continuing with the local file,
        by default True.
    verbose_level: int, optional
        - 0 == 'no-verbose' in wget, which "turn off verbose without being completely quiet,
            which means that error messages and basic information still get printed."
        - 1 == "default" in wget, which turn on verbose output, with all the available data. (default value)
        - 2 == "debug" in wget, which "turn on debug output, meaning various information important to the developers of
            does not work properly. Your system administrator may have chosen to compile Wget without debug support,
            Wget if it in which this option will not work. Please note that compiling with debug support is always
            safe—Wget compiled with the debug support will not print any debug info unless requested with this option."
    n_tries: int, optional
        number of times that wget try downloading before giving up, default is 5.
        The function do not support 'inf' for retrying infinitely.
    download_speed: float, optional
        define max download speed limit in the unit of kb/s, by default have no limit.

    Returns
    -------
    ret: subprocess.CompletedProcess
        Subprocess return object.

    Raises
    ------
    ValueError
        - Raised when verbose level is not valid
        - Raised when the `dest_file_name` is not a valid file name.
        - Raised when a file with `dest_file_name` already exist in the target directory.
    Warnings
        - Raised if exception occurred in attempt to download,
            possibly a problem from adding -content-disposition as wget setting.

    Notes
    -----
    This function uses directly forms the command called in a `subprocess` by using the given inputs.
    Thus please do not use it directly with external user inputs.
    1. Not sure if exception actually occur if wget failed after n-try.
        Unlikely going to report warning, because we have not implement ways to check errors from wget.
    2. wget have some advanced function that we have not yet implemented
        For example, we could export verbose to log with: wget -o log.txt example.com
    3. Now we are only the file we want to download have the same name as the file in target directory,
        but we did not check if the two files have the same content (wget might have already take care of it).
    4. Sometimes default filename is odd without --content-disposition option,
        (e.g. https://cmshare.eea.europa.eu/s/Wme8dMPtS8LmWgk).
    5. Cannot consecutively download the same file for testing at some location.
        And no error would return if file could not be download in this scenario.
        (e.g. https://cmshare.eea.europa.eu/s/Wme8dMPtS8LmWgk)
    6. Consider returning downloaded file path

    Reference
    --------
    wget user manual: https://www.gnu.org/software/wget/manual/wget.html

    Examples
    --------
    url = 'www.example.com'
    dest_dir = 'Home_path/folder_name'
    download_via_wget(url, dest_dir)
    """
    # Check if provided url is a downloadable file. The function will raise error if url is not valid.
    # Should figure out a way to check out file name without downloading the file.
    check_download_url_via_wget(
        url, username, password, additional_credential, verbose_level, n_tries
    )
    # Put together a command
    commands = [
        "wget",
        f"--tries={n_tries}",
    ]
    # user defined file name
    if dest_file_name is not None:
        # check if dest_file_name valid, then check if dest_file_name already exist
        if not is_valid_filename(dest_file_name):
            raise ValueError(
                f"{dest_file_name} is not a valid name. Consider using {sanitize_filename(dest_file_name)}."
            )
        # Check if target directory already have a file with a name that is identical to dest_file_name
        if os.path.isfile(os.path.join(dest_dir, dest_file_name)):
            # if local file is incomplete, continue download
            # else
            raise ValueError(
                (
                    f"{dest_file_name} is exist in {dest_dir}.",
                    "Consider a different file name or check if the remote and local files are identical.",
                )
            )
        commands.append("-O")
        commands.append(dest_file_name)
    if username is not None:
        commands.append(f"--user={username}")
    if password is not None:
        commands.append(f"--password={password}")
    # For now, add whatever argument that user wants
    if additional_credential is not None:
        commands.append(f"{additional_credential}")
    if continue_if_broke:
        commands.insert(1, "--continue")
    # if verbose not default,
    if verbose_level != 1:
        if verbose_level == 0:
            # "no-verbose"
            commands.append("-nv")
        elif verbose_level == 2:
            # "debug"
            commands.append("--debug")
        else:
            raise ValueError(
                f"Verbose_level ({verbose_level}) not understood. Valid value ranges from 0 to 2"
            )
    if download_speed is not None:
        commands.append(f"--limit-rate={download_speed}k")
    # Add URL to the end
    commands.append(url)
    # Since there were no error raised in previous url check, proceed to download
    # download to working directory
    with working_directory(dest_dir):
        try:
            commands_cd = commands.copy()
            commands_cd.insert(1, "--content-disposition")
            ret = subprocess.run(commands_cd)
            # have not implement ways to check errors from wget.
        except Exception as e:
            logger.warning(
                f"{e}. wget with --content-disposition did not work. Retry without it"
            )
            ret = subprocess.run(commands)
    return ret


def batch_download_via_wget(
    lst_urls: List[str],
    dest_dir: str,
    nproc: Optional[int] = -1,
    lst_dest_file_names: Optional[List[str]] = None,
    username: Optional[str] = None,
    password: Optional[str] = None,
    additional_credential: Optional[Any] = None,
    continue_if_broke: bool = True,
    verbose_level: int = 1,
    n_tries: int = 5,
    download_speed: Optional[float] = None,
):
    """Download files from a list of url into a single directory, support parallel processing.

    Parameters
    ----------
    lst_urls : List[str]
        List containing paths to the source zip file.
    dir_dest : str
        Path to the storage directory.
    nproc : int or -1, optional
        Define how many processor to use,
        by default -1 use all available processor (n_jobs=nproc=-1).
        If using specified amount of process, nproc = a positive integer greater than 0.
        If using all but x processor, compute n = -1-x, nproc = n (e.g. all but one processor = -2).
        If bypassing joblib, nproc = 0.
    lst_dest_file_names: List[str], optional
        List containing User defined file name, the list dimension should match with that of lst_url.
    username: str, optional
    password: str, optional
    additional_credential: any, optional
        For this version, user can add any wget option in here
        (e.g. -A, or --ftp-user=username --ftp-password=pass).
    continue_if_broke: bool, optional
        Decide if download process were interrupted, whether to download by continuing with the local file,
        by default True.
    verbose_level: int, optional
        - 0 == 'no-verbose' in wget, which "turn off verbose without being completely quiet,
            which means that error messages and basic information still get printed."
        - 1 == "default" in wget, which turn on verbose output, with all the available data.
        - 2 == "debug" in wget, which "turn on debug output, meaning various information important to the developers of
            does not work properly. Your system administrator may have chosen to compile Wget without debug support,
            Wget if it in which this option will not work. Please note that compiling with debug support is always
            safe—Wget compiled with the debug support will not print any debug info unless requested with this option."
    n_tries: int, optional
        number of times that wget try downloading before giving up, default is 5.
        The function do not support 'inf' for retrying infinitely.
    download_speed: float, optional
        define max download speed limit in the unit of kb/s, by default have no limit.

    Returns
    -------
    None

    Raises
    ------
    ValueError
        - Raised when the length of lst_urls does not equal to length of lst_dest_file_names

    Notes
    -----
    This function uses the 'download_via_wget' function,
    which directly forms the command called in a `subprocess` by using the given inputs.
    Thus please do not use it directly with external user inputs.

    Examples
    --------
    lst_urls = ['www.example_1.com','www.example_2.com']
    dest_dir = 'Home_path/folder_name'
    nproc = 2
    batch_download_via_wget(lst_urls, dest_dir, nproc)
    """
    # Check if length of lst_urls is same as lst_dest_file_names
    if lst_dest_file_names is not None and (len(lst_urls) != len(lst_dest_file_names)):
        raise ValueError(
            (
                f"Length of lst_urls ({len(lst_urls)}) does not equal",
                f"to legnth of lst_dest_file_names ({len(lst_dest_file_names)})",
            )
        )
    # Batch Downloading
    if nproc == 0:
        for index, url in enumerate(lst_urls):
            dest_file_name = (
                lst_dest_file_names
                if lst_dest_file_names is None
                else lst_dest_file_names[index]
            )
            download_via_wget(
                url,
                dest_dir,
                dest_file_name,
                username,
                password,
                additional_credential,
                continue_if_broke,
                verbose_level,
                n_tries,
                download_speed,
            )
    else:
        # Define joblib process
        parallel = Parallel(n_jobs=nproc)
        parallel(
            delayed(download_via_wget)(
                url,
                dest_dir,
                lst_dest_file_names
                if lst_dest_file_names is None
                else lst_dest_file_names[index],
                username,
                password,
                additional_credential,
                continue_if_broke,
                verbose_level,
                n_tries,
                download_speed,
            )
            for index, url in enumerate(lst_urls)
        )
